﻿/*************************************************************************
 * Created by OhZee Creative, LLC
 * 2013
 * This code is licensed under Microsoft Public License (Ms-PL)
 * http://opensource.org/licenses/ms-pl
 * http://ozcontrols.codeplex.com/
 *************************************************************************/

namespace OhZeeCreative.UI.Controls
{
    using System.ComponentModel;
    using OhZeeCreative.Helpers;

#if SILVERLIGHT
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Shapes;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
#endif
#if NETFX_CORE
    using Windows.Foundation;
    using Windows.UI;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Media;
    using Windows.UI.Xaml.Shapes;
    using Windows.UI.Xaml.Media.Animation;
#endif

    /// <summary>
    /// RadialIndicator is a ContentControl that rotates its content based on its current value and the value range. An example use is the dial inside a gauge. By associating with a RadialControl, many properties such as start and angles and radii can be adjusted. It also supports off-center dials, where the angle is automatically adjusted to point to the correct location.
    /// </summary>
    [EditorBrowsableAttribute(EditorBrowsableState.Always)]
    [TemplatePart(Name = RadialIndicator.TEMPLATE_PART_Container, Type = typeof(FrameworkElement))]
    [TemplatePart(Name = RadialIndicator.TEMPLATE_PART_Content, Type = typeof(ContentPresenter))]
	public sealed class RadialIndicator : ContentControl
	{
        /// <summary>
        /// The name of the UI element that contains the ContentPresenter element in the control template. It's Canvas by default.
        /// </summary>
        public const string TEMPLATE_PART_Container = "container";

        /// <summary>
        /// The name of the ContentPresenter element
        /// </summary>
        public const string TEMPLATE_PART_Content = "content";

		private bool isLoaded;
		private bool isScalingSet;
        private FrameworkElement container;
		private ContentPresenter contentPresenter;

		private static Point origin = new Point(0.0, 0.5);

		public static readonly DependencyProperty ValueBeginProperty = DependencyProperty.Register("ValueBegin", typeof(double), typeof(RadialIndicator), new PropertyMetadata(0.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(double), typeof(RadialIndicator), new PropertyMetadata(0.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty ValueEndProperty = DependencyProperty.Register("ValueEnd", typeof(double), typeof(RadialIndicator), new PropertyMetadata(100.0, GenericRenderInvalidatingPropertyChanged));

        public static readonly DependencyProperty AngleBeginProperty = DependencyProperty.Register("AngleBegin", typeof(double), typeof(RadialIndicator), new PropertyMetadata(90.0, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty AngleEndProperty = DependencyProperty.Register("AngleEnd", typeof(double), typeof(RadialIndicator), new PropertyMetadata(-270.0, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty RadiusBeginProperty = DependencyProperty.Register("RadiusBegin", typeof(double), typeof(RadialIndicator), new PropertyMetadata(200.0, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty RadiusEndProperty = DependencyProperty.Register("RadiusEnd", typeof(double), typeof(RadialIndicator), new PropertyMetadata(200.0, GenericRenderInvalidatingPropertyChanged));

		public static readonly DependencyProperty ScalingEnabledProperty = DependencyProperty.Register("ScalingEnabled", typeof(bool), typeof(RadialIndicator), new PropertyMetadata(true, GenericRenderInvalidatingPropertyChanged));
        public static readonly DependencyProperty ScaleEasingProperty = DependencyProperty.Register("ScaleEasing", typeof(EasingFunctionBase), typeof(RadialIndicator), new PropertyMetadata(null, GenericRenderInvalidatingPropertyChanged));

		public static readonly DependencyProperty OffsetXProperty = DependencyProperty.Register("OffsetX", typeof(double), typeof(RadialIndicator), new PropertyMetadata(0.0, GenericRenderInvalidatingPropertyChanged));
		public static readonly DependencyProperty OffsetYProperty = DependencyProperty.Register("OffsetY", typeof(double), typeof(RadialIndicator), new PropertyMetadata(0.0, GenericRenderInvalidatingPropertyChanged));

		private static void GenericRenderInvalidatingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
		{
			RadialIndicator radialIndicator = (RadialIndicator)d;
			radialIndicator.Update();
		}

		#region Properties for dependency properties

        /// <summary>
        /// Gets or sets the value associated with the starting angle of the associated RadialControl.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double ValueBegin
		{
			get { return (double)this.GetValue(ValueBeginProperty); }
			set { this.SetValue(ValueBeginProperty, value); }
		}

        /// <summary>
        /// Gets or sets the current value of the indicator.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double Value
		{
			get { return (double)this.GetValue(ValueProperty); }
			set { this.SetValue(ValueProperty, value); }
		}

        /// <summary>
        /// Gets or sets the value associated with the end angle of the associated RadialControl.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
		public double ValueEnd
		{
			get { return (double)this.GetValue(ValueEndProperty); }
			set { this.SetValue(ValueEndProperty, value); }
		}

        /// <summary>
        /// Gets or sets the length of the radius at the start angle, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public double RadiusBegin
        {
            get { return (double)this.GetValue(RadiusBeginProperty); }
            set { this.SetValue(RadiusBeginProperty, value); }
        }

        /// <summary>
        /// Gets or sets the length of the radius at the end angle, in pixels.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public double RadiusEnd
        {
            get { return (double)this.GetValue(RadiusEndProperty); }
            set { this.SetValue(RadiusEndProperty, value); }
        }

        /// <summary>
        /// Gets or sets the starting angle in degrees according to Cartesian coordinates.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public double AngleBegin
        {
            get { return (double)this.GetValue(AngleBeginProperty); }
            set { this.SetValue(AngleBeginProperty, value); }
        }

        /// <summary>
        /// Gets or sets the ending angle in degrees according to Cartesian coordinates.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public double AngleEnd
        {
            get { return (double)this.GetValue(AngleEndProperty); }
            set { this.SetValue(AngleEndProperty, value); }
        }

        /// <summary>
        /// Toggles scaling of the indicator on/off.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Always)]
        public bool ScalingEnabled
        {
            get { return (bool)this.GetValue(ScalingEnabledProperty); }
            set { this.SetValue(ScalingEnabledProperty, value); }
        }


        /// <summary>
        /// Gets or sets the horizontal offset of the indicator in pixels relative to the origin.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double OffsetX
		{
			get { return (double)this.GetValue(OffsetXProperty); }
			set { this.SetValue(OffsetXProperty, value); }
		}
        
        /// <summary>
        /// Gets or sets the vertical offset of the indicator in pixels relative to the origin.
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
		public double OffsetY
		{
			get { return (double)this.GetValue(OffsetYProperty); }
			set { this.SetValue(OffsetYProperty, value); }
		}

        /// <summary>
        /// Gets or sets the easing that determines how to scale if scaling is enabled
        /// </summary>
        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public EasingFunctionBase ScaleEasing
        {
            get { return (EasingFunctionBase)this.GetValue(ScaleEasingProperty); }
            set { this.SetValue(ScaleEasingProperty, value); }
        }

		#endregion

		public RadialIndicator()
		{
			this.DefaultStyleKey = typeof(RadialIndicator);
			this.Loaded += RadialIndicator_Loaded;
			this.SizeChanged += RadialIndicator_SizeChanged;
		}

		void RadialIndicator_SizeChanged(object sender, SizeChangedEventArgs e)
		{
			if (this.contentPresenter != null)
			{
				FrameworkElement feContent = this.Content as FrameworkElement;

				if (feContent == null)
				{
					LayoutHelper.ClearTranslate(this.contentPresenter);
				}
				else
				{
					LayoutHelper.SetTranslate(this.contentPresenter, null, -feContent.ActualHeight * 0.5);
				}
			}			
		}

		private void RadialIndicator_Loaded(object sender, RoutedEventArgs e)
		{
			this.isLoaded = true;
		}

#if SILVERLIGHT
        public override void OnApplyTemplate()
#endif
#if NETFX_CORE
		protected override void OnApplyTemplate()
#endif
		{
            this.container = this.GetTemplateChild(RadialIndicator.TEMPLATE_PART_Container) as FrameworkElement;
			this.contentPresenter = this.GetTemplateChild(RadialIndicator.TEMPLATE_PART_Content) as ContentPresenter;

			if (!this.isLoaded && this.Content == null)
			{
				this.SetContentDefault();	
			}
            this.Update();
		}

		private void SetContentDefault()
		{
			Line line = new Line()
			{
				Stroke = this.Foreground,
				X2 = 190.0,
				HorizontalAlignment = HorizontalAlignment.Stretch,
				StrokeThickness = 10.0,
				StrokeStartLineCap = PenLineCap.Round,
				StrokeEndLineCap = PenLineCap.Triangle,
                Margin = new Thickness(0.0, 2.5, 0.0, 0.0)  // StrokeThickness * 0.25
			};

            this.Content = line;

#if FALSE
			this.Content = new Rectangle()
			{
				Fill = new SolidColorBrush(Colors.Orange),
				Width = 400.0,
				Height = 150.0,
			};
#endif

#if FALSE
			this.Content = XamlReader.Load("<Rectangle xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" Width=\"200\" Height=\"10\" Fill=\"AliceBlue\"/>");
#endif
		}

		private void Update()
		{
            FrameworkElement content = this.container as FrameworkElement;
			if (content == null)
			{
				return;
			}

			double angle = 0.0;
			double normalizedValue = 0.0;

			normalizedValue = MathHelper.NormalValue(this.ValueBegin, this.ValueEnd, this.Value);
            angle = MathHelper.ActualValue(this.AngleBegin, this.AngleEnd, normalizedValue);

			if (this.OffsetX != 0.0 || this.OffsetY != 0.0)
			{
                double radius = MathHelper.ActualValue(this.RadiusBegin, this.RadiusEnd, normalizedValue);
				double radians = MathHelper.Radians(angle);
				double adjustedAngleInRadians = MathHelper.AdjustedAngle(radians, radius, this.OffsetX, -this.OffsetY);
				angle = MathHelper.Degrees(adjustedAngleInRadians);
			}

			LayoutHelper.SetRotation(content, angle);

            if (this.ScalingEnabled || this.isScalingSet)
            {
                double scaleX = 1.0;
                if (this.ScalingEnabled)
                {
                    // Define scaleX = 1 at RadiusBegin. If RadiusEnd > RadiusBegin, scaleX > 1, and vice versa.
                    if (this.RadiusBegin == 0.0)
                    {
                        scaleX = 1.0;
                    }
                    else
                    {
                        if (this.ScaleEasing == null)
                        {
                            double radiusAtValue = MathHelper.ActualValue(this.RadiusBegin, this.RadiusEnd, normalizedValue);
                            scaleX = radiusAtValue / this.RadiusBegin;
                        }
                        else
                        {
                            scaleX = 1.0 + this.ScaleEasing.Ease(normalizedValue);
                        }
                    }
                }
                LayoutHelper.SetScale(content, scaleX, scaleY: 1.0);
                this.isScalingSet = this.ScalingEnabled; // This enables us to reset scale if turned off later
            }
		}
	}
}
